PostgreSQL Listen-Notify 机制
Postgresql提供 监听 - 通知 的订阅服务,使得数据库客户端向服务端的指定通道注册为监听客户端,服务端在发出Notify通知时,所有已注册的监听客户端将能够收到该通知。
使用示例
服务端
1 | -- 不带消息的通知 |
客户端
1 | -- 不带消息的通知 |
注意
- 上面的例子看起来并不像发布-订阅,因为客户端LISTEN后并不会被主动通知,而是每次要服务端NOTIFY后,客户端再去LISTEN,这其实是误会——Postgresql客户端检测通知事件的方式取决于该客户端底层应用的编程接口。支持同步轮询异步通知两种方式。很多第三方库是支持异步通知的。
- 通道名可以使任意有效字符串,通道不需要预先创建。
命令介绍
LISTEN
1 | LISTEN channel (channel值任意) |
命令干了啥
将当前会话注册为一个channel的监听者,如果当前会话已经注册,则啥都不干
何时被通知
无论何时当命令 NOTIFY channel 被调用时,注册在该channel上的所有监听者都会被通知。
合适终止监听
当调用UNLISTEN或会话关闭时,监听终止
监听原理
客户端检测通知事件使用的方法取决于它使用的 PostgreSQL 应用程序编程接口。
- 如果使用libpq库,只是相当于定期执行SQL命令调用PQnotifies方法查询是否有新的通知
- 其它接口如libpgtcl提供更高级的方法来处理通知
事务相关性
LISTEN命令在事务提交时生效,如果执行LISTEN的事务最终回滚了,则LISTEN不会生效。
NOTIFY
1 | NOTIFY channel [, payload] (payload默认要求小于8000字节, 且必须是常量) |
命令干了啥
NOTIFY向每一个在channel上注册的客户端发送通知,通知可附带可选的字符串载荷payload。通知对所有用户都是可见的。
通知信息包括
- 通知的channel名
- 发出通知的服务端的进程PID
- 可选的payload字符串,没有时则为空字符串
channel命名规则
channel没有具体的命名规则。一般来说,channel会和某个表名一致,这样对该channel发出通知在语义上表示“我修改了这个表,看看有什么新内容吧”(配套的是将这样的NOTIFY语句放入表更新的触发器中)。不过还是要具体情况具体分析
事务相关性
如果NOTIFY用于事务中,则只有当事务提交时才会真正执行通知,如果事务取消则压根不会通知。
如果一个待接收通知的监听会话正处于事务中,则通知事件会等到它的事务处结束后在发送到它。这是合理的,因为通知发送了就不能取消,如果在事务中发送或接收,遇到想要取消通知的情况就没办法了。
这样的缺点导致了实时性不好,因此如果要求实时性高,就要把事务写短一点。
通知折叠规则
- 一个事务中对同一个channel发送多个paylaod一样的通知,可能会被折叠成一个通知
- 一个事务中对同一个channel发送不同的payload通知,不会执行折叠操作
- 不同事务,无论channel和payload是否重复,都不会执行折叠操作
顺序
NOTIFY保证顺序
- 同一事务中,通知发出的顺序按NOTIFY声明的顺序进行
- 不同事务,通知发出的顺序按事务提交的顺序走
避免额外的工作
在一个客户端上发送NOTIFY命令,同时该客户端监听同样的channel,会出现通知发给自己的情况,此时可以通过通知中带的进程PID发现发送者就是自己,从而忽略本来要执行的逻辑,节省性能。
队列
有一个队列用于存储已经NOTIFY但还没被所有注册的客户端获取的通知,如果队列满了,则NOTIFY在提交时会失败。
该队列默认大小为8GB,足够大多数情况。
如果一个监听会话长时间处于事务中,则对应通知会持续累积,当队列满了一半时,会打出警告日志。
pg_notification_queue_usage函数可以查看用了多少。
pg_notify(channel, payload)
该方法执行同样的事情,好处是payload不再要求必须是常量,可以通过变量运算得到。
UNLISTEN
1 | UNLISTEN { channel | *)} |
用于将当前会话移除出channel的监听列表,*表示移除当前会话监听的所有channel。
在会话期间,使用UNLISTEN channel的方式移除监听
在会话结束时,会自动执行UNLISTEN *